﻿#region " LICENSE "
// DRILL - Dependency Resolution Interface Layer Library
// Copyright (c) 2008-2012 Wade A. Reynolds. All rights reserved.
// http://drill.codeplex.com/
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Drill.Core.Utilities.Extensions;
using Drill.Extensibility;

namespace Drill.Configuration
{
	/// <summary>
	/// Reads Drill XML configuration directly into the configuration object graph.
	/// </summary>
	public class XmlConfigurationReader
	{
		private readonly XDocument _xdocument;

		private XmlConfigurationReader(XDocument xdocument)
		{
			_xdocument = xdocument;
		}

		#region " Static From... Methods "

		/// <summary>
		/// Creates a new instance of <see cref="XmlConfigurationReader"/> that obtains 
		/// the XML configuration from the specified file.
		/// </summary>
		/// <param name="filename">The filename of the file that contains the XML configuration.</param>
		/// <returns>A new instance of <see cref="XmlConfigurationReader"/> that reads the XML configuration.</returns>
		/// <exception cref="System.ArgumentNullException">filename</exception>
		public static XmlConfigurationReader FromFile(string filename)
		{
			if (string.IsNullOrWhiteSpace(filename))
			{
				throw new ArgumentNullException("filename");
			}
			XDocument xdocument = XDocument.Load(filename);
			return new XmlConfigurationReader(xdocument);
		}

		/// <summary>
		/// Creates a new instance of <see cref="XmlConfigurationReader"/> that obtains 
		/// the XML configuration from the specified embedded manifest resource.
		/// </summary>
		/// <param name="manifestResourceName">
		/// Name of the manifest resource that contains the XML configuration.
		/// </param>
		/// <param name="assembly">
		/// The assembly in which the manifest resource resides or null to use the calling assembly.
		/// </param>
		/// <returns>A new instance of <see cref="XmlConfigurationReader"/> that reads the XML configuration.</returns>
		/// <exception cref="System.ArgumentNullException">manifestResourceName</exception>
		public static XmlConfigurationReader FromManifestResource(string manifestResourceName, Assembly assembly = null)
		{
			if (string.IsNullOrWhiteSpace(manifestResourceName))
			{
				throw new ArgumentNullException("manifestResourceName");
			}
			if (null == assembly)
			{
				assembly = Assembly.GetCallingAssembly();
			}
			Stream stream = assembly.GetManifestResourceStream(manifestResourceName);
			return FromStream(stream);
		}

		/// <summary>
		/// Creates a new instance of <see cref="XmlConfigurationReader"/> that obtains 
		/// the XML configuration from the specified <see cref="Stream"/> instance.
		/// </summary>
		/// <param name="stream">The <see cref="Stream"/> instance that contains the XML configuration.</param>
		/// <returns>A new instance of <see cref="XmlConfigurationReader"/> that reads the XML configuration.</returns>
		/// <exception cref="System.ArgumentNullException">stream</exception>
		public static XmlConfigurationReader FromStream(Stream stream)
		{
			if (null == stream)
			{
				throw new ArgumentNullException("stream");
			}
			XDocument xdocument = XDocument.Load(stream);
			return new XmlConfigurationReader(xdocument);
		}

		/// <summary>
		/// Creates a new instance of <see cref="XmlConfigurationReader"/> that obtains 
		/// the XML configuration from the specified <see cref="XDocument"/> instance.
		/// </summary>
		/// <param name="xdocument">The <see cref="XDocument"/> instance that contains the XML configuration.</param>
		/// <returns>A new instance of <see cref="XmlConfigurationReader"/> that reads the XML configuration.</returns>
		/// <exception cref="System.ArgumentNullException">xdocument</exception>
		public static XmlConfigurationReader FromXDocument(XDocument xdocument)
		{
			if (null == xdocument)
			{
				throw new ArgumentNullException("xdocument");
			}
			return new XmlConfigurationReader(xdocument);
		}

		/// <summary>
		/// Creates a new instance of <see cref="XmlConfigurationReader"/> that obtains 
		/// the XML configuration from the specified XML string.
		/// </summary>
		/// <param name="xml">The string that represents the XML configuration.</param>
		/// <returns>A new instance of <see cref="XmlConfigurationReader"/> that reads the XML configuration.</returns>
		/// <exception cref="System.ArgumentNullException">xml</exception>
		public static XmlConfigurationReader FromXmlString(string xml)
		{
			if (string.IsNullOrWhiteSpace(xml))
			{
				throw new ArgumentNullException("xml");
			}
			XDocument xdocument = XDocument.Parse(xml);
			return new XmlConfigurationReader(xdocument);
		}

		#endregion " Static From... Methods "

		/// <summary>
		/// Reads all dependency resolver configuration instances.
		/// </summary>
		/// <returns>
		/// An instance of <see cref="IDependencyResolverConfigurationCollection"/> 
		/// that contains the configuration data.
		/// </returns>
		public IDependencyResolverConfigurationCollection ReadAllDependencyResolversConfiguration()
		{
			DependencyResolverConfigurationCollection collection = new DependencyResolverConfigurationCollection();
			IEnumerable<XElement> elements = FindDependencyResolverElements(_xdocument);
			foreach (var element in elements)
			{
				var config = CreateDependencyResolverConfiguration(element);
				if (null != config)
				{
					collection.Add(config);
				}
			}
			return collection;
		}

		/// <summary>
		/// Reads the dependency resolver configuration with the specified name.
		/// </summary>
		/// <param name="name">The configured name of the dependency resolver configuration to be read.</param>
		/// <returns>
		/// An instance of <see cref="IDependencyResolverConfiguration"/> that contains the configuration data.
		/// </returns>
		public IDependencyResolverConfiguration ReadDependencyResolverConfiguration(string name)
		{
			IEnumerable<XElement> dependencyResolverElements = FindDependencyResolverElements(_xdocument);
			XElement dependencyResolverElement = dependencyResolverElements.FirstOrDefault(e =>
				{
					XAttribute a = e.Attribute("name");
					return (null != a) && string.Compare(a.Value, name, StringComparison.OrdinalIgnoreCase) == 0;
				});
			return CreateDependencyResolverConfiguration(dependencyResolverElement);
		}

		private static IDependencyResolverConfiguration CreateDependencyResolverConfiguration(XElement element)
		{
			DependencyResolverConfiguration config = new DependencyResolverConfiguration();
			config.DrillBits = CreateDrillBitConfigurations(element);
			config.Enabled = ReadEnabledAttributeValue(element);
			config.Name = ReadNameAttributeValue(element);
			config.RootPathProviders = CreateRootPathProviderConfigurations(element);
			config.SearchPaths = CreateSearchPathConfigurations(element);
			config.Type = ReadTypeAttributeValue(element);
			return config;
		}

		private static IDrillBitConfigurationCollection CreateDrillBitConfigurations(XElement parentElement)
		{
			DrillBitConfigurationCollection collection = new DrillBitConfigurationCollection();
			IEnumerable<XElement> elements =
				parentElement
					.Elements("drillBits")
					.Elements("drillBit");
			foreach (var element in elements)
			{
				var config = CreateDrillBitConfiguration(element);
				if (null != config)
				{
					collection.Add(config);
				}
			}
			return collection;
		}

		private static IDrillBitConfiguration CreateDrillBitConfiguration(XElement element)
		{
			DrillBitConfiguration config = new DrillBitConfiguration();
			config.ConfigurationSources = CreateConfigSourceConfigurations(element);
			config.Enabled = ReadEnabledAttributeValue(element);
			config.Name = ReadNameAttributeValue(element);
			config.Type = ReadTypeAttributeValue(element);
			return config;
		}

		private static IConfigSourceConfigurationCollection CreateConfigSourceConfigurations(XElement parentElement)
		{
			ConfigSourceConfigurationCollection collection = new ConfigSourceConfigurationCollection();
			IEnumerable<XElement> elements =
				parentElement
					.Elements("cfgSources")
					.Elements("cfgSource");
			foreach (var element in elements)
			{
				var config = CreateConfigSourceConfiguration(element);
				if (null != config)
				{
					collection.Add(config);
				}
			}
			return collection;
		}

		private static IConfigSourceConfiguration CreateConfigSourceConfiguration(XElement element)
		{
			ConfigSourceConfiguration config = new ConfigSourceConfiguration();
			config.Enabled = ReadEnabledAttributeValue(element);
			config.Key = ReadKeyAttributeValue(element);
			config.Method = ReadConfigurationMethodAttributeValue(element);
			config.Value = ReadValueAttributeValue(element);
			return config;
		}

		private static IRootPathProviderConfigurationCollection CreateRootPathProviderConfigurations(XElement parentElement)
		{
			RootPathProviderConfigurationCollection collection = new RootPathProviderConfigurationCollection();
			IEnumerable<XElement> elements =
				parentElement
					.Elements("rootPathProviders")
					.Elements("rootPathProvider");
			foreach (var element in elements)
			{
				var config = CreateRootPathProviderConfiguration(element);
				if (null != config)
				{
					collection.Add(config);
				}
			}
			return collection;
		}

		private static IRootPathProviderConfiguration CreateRootPathProviderConfiguration(XElement element)
		{
			RootPathProviderConfiguration config = new RootPathProviderConfiguration();
			config.Enabled = ReadEnabledAttributeValue(element);
			config.Type = ReadTypeAttributeValue(element);
			return config;
		}

		private static ISearchPathConfigurationCollection CreateSearchPathConfigurations(XElement parentElement)
		{
			SearchPathConfigurationCollection collection = new SearchPathConfigurationCollection();
			IEnumerable<XElement> elements =
				parentElement
					.Elements("searchPaths")
					.Elements("searchPath");
			foreach (var element in elements)
			{
				var config = CreateSearchPathConfiguration(element);
				if (null != config)
				{
					collection.Add(config);
				}
			}
			return collection;
		}

		private static ISearchPathConfiguration CreateSearchPathConfiguration(XElement element)
		{
			SearchPathConfiguration config = new SearchPathConfiguration();
			config.Enabled = ReadEnabledAttributeValue(element);
			config.Path = ReadPathAttributeValue(element);
			return config;
		}

		private static IEnumerable<XElement> FindDependencyResolverElements(XDocument xdocument)
		{
			List<string> elementNames =
				new List<string>
					{
							"configuration"
						  , "drill"
						  , "dependencyResolvers"
						  , "dependencyResolver"
					};

			List<XElement> elements = null;

			while (elementNames.Count > 0)
			{
				elements = FindDependencyResolverElementsUsingElementNamesList(new List<XElement> { xdocument.Root }, elementNames);

				if (null == elements || elements.Count < 1)
				{
					// Remove the first elementName and try again.
					elementNames.RemoveAt(0);
				}
				else
				{
					break;
				}
			}

			return elements;
		}

		private static List<XElement> FindDependencyResolverElementsUsingElementNamesList(List<XElement> elements, IEnumerable<string> elementNames)
		{
			foreach (string elementName in elementNames)
			{
				string localName = elementName;
				if (elements.Any())
				{
					List<XElement> foundElements = (from x in elements where x.Name.LocalName == localName select x).ToList();
					if (false == foundElements.Any())
					{
						foundElements = elements.Elements(localName).ToList();
					}
					elements = foundElements;
				}
			}
			return elements;
		}

		private static string ReadAttributeValue(XElement element, string attributeName)
		{
			XAttribute attribute = element.Attribute(attributeName);
			return (null != attribute) ? attribute.Value : null;
		}

		private static string ReadNameAttributeValueNotAllowingEmptyOrWhitespace(XElement element, string attributeName)
		{
			string value = ReadAttributeValue(element, attributeName);
			if (string.IsNullOrWhiteSpace(value))
			{
				value = null;
			}
			return value;
		}

		private static bool ReadEnabledAttributeValue(XElement element, bool defaultValue = true)
		{
			string value = ReadAttributeValue(element, "enabled");
			if (false == string.IsNullOrWhiteSpace(value))
			{
				return value.ToBoolean();
			}
			return defaultValue;
		}

		private static string ReadKeyAttributeValue(XElement element)
		{
			return ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "key");
		}

		private static DrillBitConfigMethod ReadConfigurationMethodAttributeValue(XElement element)
		{
			string methodString = ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "method");
			DrillBitConfigMethod method;
			return Enum.TryParse(methodString, true, out method) ? method : DrillBitConfigMethod.Unknown;
		}

		private static string ReadNameAttributeValue(XElement element)
		{
			return ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "name");
		}

		private static string ReadPathAttributeValue(XElement element)
		{
			return ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "path");
		}

		private static string ReadTypeAttributeValue(XElement element)
		{
			return ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "type");
		}

		private static string ReadValueAttributeValue(XElement element)
		{
			return ReadNameAttributeValueNotAllowingEmptyOrWhitespace(element, "value");
		}
	}
}